﻿using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Windows.Controls;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Babylon.Toolbox
{
    public partial class Scene : IDisposable
    {
        readonly DrawingSurface drawingSurface;
        readonly GraphicsDevice graphicsDevice;
        public Color BackgroundColor { get; set; }
        public Color AmbientColor { get; set; }
        public StatesManager StatesManager { get; private set; }

        public CameraBase Camera { get; set; }

        public Scene(DrawingSurface surface, GraphicsDevice device)
        {
            ParticleSystems = new ObservableCollection<ParticleSystem>();
            Objects = new ObservableCollection<SceneObject>();
            BackgroundColor = new Color(0, 0, 0, 0);
            AmbientColor = new Color(0.3f, 0.3f, 0.3f, 0);
            graphicsDevice = device;
            drawingSurface = surface;

            drawingSurface.SizeChanged += drawingSurface_SizeChanged;

            Objects.CollectionChanged += Objects_CollectionChanged;
        }

        void drawingSurface_SizeChanged(object sender, System.Windows.SizeChangedEventArgs e)
        {
            if (Camera == null)
                return;

            Camera.AspectRatio = (float)(drawingSurface.ActualWidth / drawingSurface.ActualHeight);
        }

        public void CreateDefaultOrbitCamera(string name)
        {
            BoundingSphere boundingSphere = new BoundingSphere();

            boundingSphere = Objects.Aggregate(boundingSphere, (current, sceneObject) => BoundingSphere.CreateMerged(current, sceneObject.BoundingSphere));

            OrbitCamera camera = new OrbitCamera(name)
            {
                Alpha = -0.4f,
                Radius = boundingSphere.Radius * 2.0f,
                Target = boundingSphere.Center
            };

            Camera = camera;
        }

        static bool IsRenderable(SceneObject sceneObject, BoundingFrustum boundingFrustum)
        {
            if (!sceneObject.IsVisible)
                return false;

            BoundingSphere boundingSphere = sceneObject.BoundingSphere.Transform(sceneObject.World);

            return boundingFrustum.Contains(boundingSphere) != ContainmentType.Disjoint;
        }

        public void Draw(TimeSpan totalTime)
        {
            if (Camera == null)
                throw new NullReferenceException(Strings.CameraMustBeSet);

            BoundingFrustum boundingFrustum = new BoundingFrustum(Camera.View * Camera.Projection);

            if (StatesManager == null)
                StatesManager = StatesManager.FromDevice(graphicsDevice);

            // States
            StatesManager.DepthBufferEnable = true;
            StatesManager.CullMode = CullMode.None;
            StatesManager.ApplyDepthStencilState();
            StatesManager.ApplyRasterizerState();

            // Render
            graphicsDevice.Clear(ClearOptions.Target | ClearOptions.DepthBuffer, BackgroundColor, 1.0f, 0);

            lock (this) // protect collections
            {
                // Opaques
                graphicsDevice.BlendState = BlendState.Opaque;
                foreach (SceneObject sceneObject in opaqueObjects)
                {
                    if (IsRenderable(sceneObject, boundingFrustum))
                        sceneObject.Draw(this);
                }

                // Alpha blended
                graphicsDevice.BlendState = BlendState.AlphaBlend;
                foreach (SceneObject sceneObject in alphaBlendedObjects)
                {
                    if (IsRenderable(sceneObject, boundingFrustum))
                        sceneObject.Draw(this);
                }
            }

            // Particles
            var ellapsed = (float)totalTime.TotalSeconds;
            for (int index = 0; index < ParticleSystems.Count; index++) // use for loop to enable ParticleSystems modifications while rendering
            {
                ParticleSystem particleSystem = ParticleSystems[index];
                particleSystem.UpdateAndDraw(graphicsDevice, ellapsed, Camera.View, Camera.Projection);
            }


            // Camera
            OrbitCamera orbitCamera = Camera as OrbitCamera;

            if (orbitCamera != null)
                orbitCamera.ApplyInertia();
        }

        public bool Pick(int x, int y, out float distance, out SceneObject pickedObject, out ModelMesh pickedMesh)
        {
            distance = float.MaxValue;
            pickedMesh = null;
            pickedObject = null;

            foreach (SceneObject sceneObject in Objects)
            {
                Ray ray = Utilities.CreateRay(x, y, (float)drawingSurface.ActualWidth, (float)drawingSurface.ActualHeight, sceneObject.World, Camera.View, Camera.Projection);
                foreach (ModelMesh mesh in sceneObject.Model.Meshes)
                {
                    if (!ray.Intersects(mesh.BoundingSphere).HasValue)
                        continue;

                    float localDistance;

                    if (!mesh.Intersects(ray, out localDistance))
                        continue;

                    if (localDistance >= distance)
                        continue;

                    distance = localDistance;
                    pickedMesh = mesh;
                    pickedObject = sceneObject;
                }
            }

            return (distance != float.MaxValue);
        }

        public void Dispose()
        {
            drawingSurface.SizeChanged -= drawingSurface_SizeChanged;
        }
    }
}
